﻿using System;
using System.Collections.Generic;
using System.Text;
using SlimDX;
using SlimDX.Direct3D9;

namespace Nimue.Misc
{
    enum CameraMode
    {
        FirstPerson,
        ThirdPerson
    }
    class ExtendedCamera
    {
        #region Fields
        private Vector3 location;
        private Vector3 target;
        private Vector3 forwardDirection;
        private float fieldOfView;
        private float aspectRatio;
        private float nearClip;
        private float farClip;
        private Plane nearPlane, farPlane, leftPlane, rightPlane, topPlane, bottomPlane;
        private Matrix view;
        private Matrix projection;
        private bool viewChanged = true;
        private bool projectionChanged = true;
        private bool frustumChanged = true;
        private CameraMode cameraMode = CameraMode.FirstPerson;
        #endregion

        #region Public Properties
        public Vector3 Location
        {
            get { return location; }
            set
            {
                if (location == value)
                    return;

                location = value;
                viewChanged = true;
                frustumChanged = true;
            }
        }

        public Vector3 Target
        {
            get { return target; }
            set
            {
                if (target == value)
                    return;

                target = value;
                viewChanged = true;
                frustumChanged = true;
            }
        }

        public float FieldOfView
        {
            get { return fieldOfView; }
            set
            {
                if (fieldOfView == value)
                    return;

                fieldOfView = value;
                projectionChanged = true;
                frustumChanged = true;
            }
        }

        public float AspectRatio
        {
            get { return aspectRatio; }
            set
            {
                if (aspectRatio == value)
                    return;

                aspectRatio = value;
                projectionChanged = true;
                frustumChanged = true;
            }
        }

        public float NearClip
        {
            get { return nearClip; }
            set
            {
                if (nearClip == value)
                    return;

                nearClip = value;
                projectionChanged = true;
                frustumChanged = true;
            }
        }

        public float FarClip
        {
            get { return farClip; }
            set
            {
                if (farClip == value)
                    return;

                farClip = value;
                projectionChanged = true;
                frustumChanged = true;
            }
        }

        public Vector3 ForwardDirection
        {
            get
            {
                if (viewChanged)
                    RebuildViewMatrix();
                return forwardDirection;
            }
        }

        public Matrix ViewMatrix
        {
            get
            {
                if (viewChanged)
                    RebuildViewMatrix();
                return view;
            }
        }

        public Matrix ProjectionMatrix
        {
            get
            {
                if (projectionChanged)
                    RebuildProjectionMatrix();
                return projection;
            }
        }

        public CameraMode CameraMode
        {
            get { return cameraMode; }
            set { cameraMode = value; }
        }
        #endregion

        #region Constructor
        public ExtendedCamera()
        {
            location = Vector3.Zero;
            target = Vector3.UnitZ;
        }
        #endregion

        #region Camera Manipulation
        public void Rotate(float deltaX, float deltaY)
        {
            switch (cameraMode)
            {
                case CameraMode.FirstPerson:
                    RotateFirstPerson(deltaX, deltaY);
                    break;
                case CameraMode.ThirdPerson:
                    RotateThirdPerson(deltaX, deltaY);
                    break;
                default:
                    break;
            }
        }

        private void RotateFirstPerson(float deltaX, float deltaY)
        {
            Vector3 axis = Vector3.Cross(target - location, Vector3.UnitY);
            axis.Normalize();

            Matrix rot = Matrix.RotationAxis(axis, deltaY);
            Vector4 transformed = Vector3.Transform(target - location, rot);
            target = new Vector3(transformed.X, transformed.Y, transformed.Z) + location;

            rot = Matrix.RotationAxis(Vector3.UnitY, deltaX);
            transformed = Vector3.Transform(target - location, rot);
            target = new Vector3(transformed.X, transformed.Y, transformed.Z) + location;
        }

        private void RotateThirdPerson(float deltaX, float deltaY)
        {
            Vector3 axis = Vector3.Cross(target - location, Vector3.UnitY);
            axis.Normalize();

            Matrix rot = Matrix.RotationAxis(axis, deltaY);
            Vector4 transformed = Vector3.Transform(target - location, rot);
            location = new Vector3(transformed.X, transformed.Y, transformed.Z) + target;

            rot = Matrix.RotationAxis(Vector3.UnitY, deltaX);
            transformed = Vector3.Transform(target - location, rot);
            location = new Vector3(transformed.X, transformed.Y, transformed.Z) + target;
        }
        #endregion

        #region Matrix Construction
        protected void RebuildViewMatrix()
        {
            view = Matrix.LookAtLH(Location, Target, Vector3.UnitY);
            if ((forwardDirection = (Target - Location)) != Vector3.Zero)
                forwardDirection.Normalize();
            viewChanged = false;
        }

        protected void RebuildProjectionMatrix()
        {
            projection = Matrix.PerspectiveFovLH(FieldOfView, AspectRatio, NearClip, FarClip);
            projectionChanged = false;
        }
        #endregion

        #region Frustum Culling
        public bool PerformFrustumCulling(BoundingBox box)
        {
            if (frustumChanged)
                RebuildFrustum();

            if (Plane.Intersects(nearPlane, box) == PlaneIntersectionType.Back)
                return false;
            if (Plane.Intersects(farPlane, box) == PlaneIntersectionType.Back)
                return false;
            if (Plane.Intersects(leftPlane, box) == PlaneIntersectionType.Back)
                return false;
            if (Plane.Intersects(rightPlane, box) == PlaneIntersectionType.Back)
                return false;
            if (Plane.Intersects(topPlane, box) == PlaneIntersectionType.Back)
                return false;
            if (Plane.Intersects(bottomPlane, box) == PlaneIntersectionType.Back)
                return false;

            return true;
        }
        #endregion

        #region Frustum Construction
        private void RebuildFrustum()
        {
            Matrix vp = ViewMatrix * ProjectionMatrix;
            // Left clipping plane
            leftPlane.Normal.X = vp.M14 + vp.M11;
            leftPlane.Normal.Y = vp.M24 + vp.M21;
            leftPlane.Normal.Z = vp.M34 + vp.M31;
            leftPlane.D = vp.M44 + vp.M41;
            // Right clipping plane
            rightPlane.Normal.X = vp.M14 - vp.M11;
            rightPlane.Normal.Y = vp.M24 - vp.M21;
            rightPlane.Normal.Z = vp.M34 - vp.M31;
            rightPlane.D = vp.M44 - vp.M41;
            // Top clipping plane
            topPlane.Normal.X = vp.M14 - vp.M12;
            topPlane.Normal.Y = vp.M24 - vp.M22;
            topPlane.Normal.Z = vp.M34 - vp.M32;
            topPlane.D = vp.M44 - vp.M42;
            // Bottom clipping plane
            bottomPlane.Normal.X = vp.M14 + vp.M12;
            bottomPlane.Normal.Y = vp.M24 + vp.M22;
            bottomPlane.Normal.Z = vp.M34 + vp.M32;
            bottomPlane.D = vp.M44 + vp.M42;
            // Near clipping plane
            nearPlane.Normal.X = vp.M13;
            nearPlane.Normal.Y = vp.M23;
            nearPlane.Normal.Z = vp.M33;
            nearPlane.D = vp.M43;
            // Far clipping plane
            farPlane.Normal.X = vp.M14 - vp.M13;
            farPlane.Normal.Y = vp.M24 - vp.M23;
            farPlane.Normal.Z = vp.M34 - vp.M33;
            farPlane.D = vp.M44 - vp.M43;

            frustumChanged = false;
        }
        #endregion
    }
}
